package huffman;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

/**
 * @author 谭继好
 * @Description
 * @date 2024-05-21 18:34
 */
public class Utils {

    // 压缩线程，并发读文件，构建哈夫曼树，返回编码结果。分隔位置没有关系，反正都要转换
    // 这里需要显示指明长度，否则不足长度位置补空，也会写入文件
    static class ZipThread implements Callable<StringBuilder> {

        private HuffmanTree tree;
        private String path;
        private int size;

        public ZipThread(String path, int size) {
            this.path = path;
            this.size = size;
        }

        @Override
        public StringBuilder call() {

            // 读取文件，构建哈夫曼树
            String content = conReadFileWithLength(path, size);
            tree = new HuffmanTree(content);

            // 依次拼接编码后字符串及编码表
            StringBuilder res = joinInfo(tree);

            // 可能有多个片段，使用竺分隔
            return res.append("竺");
        }
    }

    // 并发解压缩，使用编码后的片段初始化
    static class UnZipThread implements Callable<StringBuilder> {

        private HuffmanTree tree;
        private String content;

        public UnZipThread(String content) {
            this.content = content;
        }

        @Override
        public StringBuilder call() {

            // 转换后字符以"李"结束，分割字符串。可能出现多个"李"，这里从第一个分开
            int spilt = content.indexOf("李"), end = content.indexOf("\n", spilt + 1);

            // 分隔映射关系字符数组
            char[] arr = content.substring(end + 1).toCharArray();

            // 恢复出映射关系，构建哈夫曼树
            tree = new HuffmanTree(initialRelation(arr));

            // 恢复出原字符串
            return tree.decode(content.substring(0, end));
        }
    }

    // 解码的时候将字符转换为对应的二进制串
    // 存储的时候正向存取，越靠后次数越高，还原的时候逆向还原，高位放后面，确保顺序正确
    public static StringBuilder char2Bin(int num) {
        StringBuilder res = new StringBuilder("0000000");
        int iter = 6;
        while (num > 0) {
            res.setCharAt(iter, (char) (num % 2 + '0'));
            num = num / 2; iter--;
        }
        return res;
    }

    // 还原映射关系表时十进制转换为对应的二进制编码
    public static StringBuilder dec2bin(int num) {
        StringBuilder res = new StringBuilder();
        while (num > 1) {
            res.append(num % 2);
            num = num / 2;
        }
        res.append(num);
        // 由于存储的时候首位多存了1，这里删除
        // 注意顺序：先删除末尾字符，然后颠倒字符串
        return res.delete(res.length() - 1, res.length()).reverse();
    }

    // 存储映射关系表，二进制转换为十进制存储
    public static int bin2dec(StringBuilder bin) {
        int res = 0, times = 1;
        for (int i = bin.length() - 1; i > -1; i--, times *= 2) {
            res += (bin.charAt(i) - '0') * times;
        }
        return res;
    }

    // 恢复出对应的映射关系，先转整数，再转二进制，构建哈希表
    public static Map<Character, StringBuilder> initialRelation(char[] arr) {
        Map<Character, StringBuilder> relation = new HashMap<>();
        char c; int iter = 0, num = 0;
        while (iter < arr.length - 1) {
            c = arr[iter]; iter++; num = 0;
            while (arr[iter] != '\n') {
                num = num * 10 + arr[iter] - '0';
                iter++;
            }
            iter++;
            relation.put(c, dec2bin(num));
        }
        return relation;
    }

    // 编码后字符串 + 对应关系。为了节省空间，将对应编码转化为对应的十进制数据
    public static StringBuilder joinInfo(HuffmanTree tree) {
        StringBuilder temp = tree.transfer().append('\n');
        Map<Character, StringBuilder> relation = tree.getRelation();
        for (Character c : relation.keySet()) {
            // 由于转换的时候0无法判断有几个，在最前面加上一个1用于判别
            temp.append("" + c + Utils.bin2dec(relation.get(c).insert(0, '1')) + "\n");
        }
        return temp;
    }

    // 读取文件全部内容
    public static String readAllFile(String path) {
        String encoding = "UTF-8";
        Long filelength = new File(path).length();
        byte[] filecontent = new byte[filelength.intValue()];
        RandomAccessFile in = null; String content = null;
        try {
            in = new RandomAccessFile(path, "r");
            in.read(filecontent);
            content = new String(filecontent, encoding);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (in != null) in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return content;
    }

    // 并发读取文件指定大小内容
    public static String conReadFileWithLength(String path, int size) {
        String encoding = "UTF-8";
        RandomAccessFile in = null; String content = null;
        try {
            in = new RandomAccessFile(path, "r");
            byte[] filecontent = new byte[size];
            in.read(filecontent, 0, size);
            content = new String(filecontent, encoding);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (in != null) in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return content;
    }

    // 将指定内容写出到指定路径文件
    // content可以是字符串，直接写出到文件，也可以是哈希表，遍历写出文件
    public static void writeToFile(String path, Object content) {
        BufferedWriter out = null; FileWriter writer = null;
        try {
            File outputFile = new File(path);
            outputFile.createNewFile();
            writer = new FileWriter(outputFile);
            out = new BufferedWriter(writer);
            if (content instanceof String) {
                out.write((String) content);
            } else if (content instanceof HashMap) {
                HashMap map = (HashMap) content;
                for (int i = 0; i < map.size(); i++) {
                    Future f = (Future) map.get(i);
                    String s = f.get().toString();
                    if (s != null && s.length() != 0) out.write(s);
                }
            }
        } catch (IOException | ExecutionException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            try {
                if (out != null) out.close();
                if (writer != null) writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
